package gamebackend;


import framework.Color;
import framework.GraphicsWrapper;
import framework.KPool;
import framework.KPool.PoolObjectFactory;
import framework.Point;
import gamebackend.Block.NeighborState;
import generators.BlockGenerator;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;


/**
 * Represents a field of rectangular walls that approaches the player.  This can be thought
 * of as a grid where the bottom-left most corner is row 0, column 0 and rows increase towards
 * the top of the screen.  Above the visible play area is the "extra area" composed of the path row, gen area
 * and fixed area.  The topmost row is the path row, where special block values are inserted
 * to indicate where FlightPaths are.  Below the path row is the gen area where the Generators
 * spawn blocks.  The fixed area is below the gen area and is where the positions of blocks no 
 * longer can change--here is where images can be assigned to blocks, for example.
 * 
 * @author Jonathan Caddey
 *
 */
public class BlockLayer implements PoolObjectFactory<Block> {

	/**
	 * The fewest rows allowable in the gen area.
	 */
	private static final int MIN_GEN_AREA_ROWS = 3;
	
	/**
	 * The fewest rows allowable in the fixed area.
	 */
	private static final int MIN_FIXED_AREA_ROWS = 2;
	
	private final int my_fixedAreaStart;
	private final int my_genAreaStart;
	private final int my_pathRow;
	
	/**
	 * The height, in rows, of the play area.
	 */
	private final int my_playAreaRows;
	
	/**
	 * The height, in rows, of the fixed area.
	 */
	private final int my_fixedAreaRows;
	
	/**
	 * The height, in rows, of the gen area.
	 */
	private final int my_genAreaRows;
	
	/**
	 * The width in user space.
	 */
	private final int my_width;
	
	/**
	 * The height in user space.
	 */
	private final int my_height;
	
	/**
	 * Height of the area above the board where blocks are generated.
	 */
	private final int my_extraHeight;
	private final int my_blocksHigh;
	private final int my_blocksWide;
	/**
	 * The width of one block
	 */
	private final float my_scaleX;
	/**
	 * The height of one block
	 */
	private final float my_scaleY;
	private final int my_totalBlocksHigh;
	private final double my_extraTop;
	private final Block[][] my_blocks;
	private final KPool<Block> my_pool;
	/**
	 * The point used to iterate over FlightPaths.
	 */
	private final Point my_flightPoint;
	/**
	 * The index of the row in my_blocks that represents the bottom of the board.
	 */
	private int my_bottom = 0;
	
	private double my_yOffset;
	/**
	 * Added when translating from grid coordinates to user space coordinates.
	 */
	private double my_fixedYOffset;
	private List<BlockGenerator> my_generators;
	private Map<BlockGenerator, Double> my_generatorLifeTimes;
	private final List<FlightPath> my_paths;
	
	/**
	 * Whether this is currently in a call to dispose.
	 */
	/*
	 * This is kind of a hack to get all the blocks
	 * out of the pool in order to dispose any
	 * bitmaps linked to the blocks.
	 */
	private boolean my_disposing;
	
	
	
	
	/**
	 * This class does not manage the paths.  Rather, it keeps a reference to the provided list
	 * and expects an external class to manage the paths.
	 * @param the_width
	 * @param the_height
	 * @param the_extraHeight how much space above the board is devoted to terrain generation
	 * @param the_blocksWide
	 * @param the_blocksHigh
	 * @param the_paths
	 * @param the_poolSize how many blocks to keep cached.
	 */
	public BlockLayer(final int the_width, final int the_height, final int the_extraHeight,
			final int the_blocksWide, int the_blocksHigh, List<FlightPath> the_paths, int the_poolSize) {
		my_flightPoint = new Point(0, 0);
		my_paths = the_paths;
		my_width = the_width;
		my_height = the_height;
		my_extraHeight = the_extraHeight;
		my_blocksHigh = the_blocksHigh;
		my_blocksWide = the_blocksWide;
		my_scaleX = (float)my_width / my_blocksWide;
	    my_scaleY = (float)my_height / my_blocksHigh;
		my_totalBlocksHigh = the_blocksHigh + Math.max(5, (int) (my_extraHeight / my_scaleY));
		my_blocks = new Block[my_totalBlocksHigh][];
		for (int row = 0; row < my_totalBlocksHigh; row++) {
			my_blocks[row] = new Block[my_blocksWide];
			for (int col = 0; col < my_blocksWide; col++) {
				my_blocks[row][col] = Block.PRIMARY_PATH;
			}
		}
		my_generators = new ArrayList<BlockGenerator>(0);
		my_generatorLifeTimes = new HashMap<BlockGenerator, Double>(0);
		my_extraTop = my_height - ((double)my_totalBlocksHigh * my_scaleY);
		
		my_pathRow = my_totalBlocksHigh - 1;
		
		my_genAreaRows = MIN_GEN_AREA_ROWS;
		my_genAreaStart = my_pathRow - my_genAreaRows;
		
		my_fixedAreaRows = MIN_FIXED_AREA_ROWS;
		my_fixedAreaStart = my_genAreaStart - my_fixedAreaRows;
		
		my_playAreaRows = my_blocksHigh + 1;
		my_pool = new KPool<Block>(this, my_blocksWide * my_blocksHigh) ;
		
		
	}
	public BlockLayer(final int the_width, final int the_height, final int the_extraHeight,
			final int the_blocksWide, final int the_blocksHigh, final int the_poolSize) {
		this(the_width, the_height, the_extraHeight, the_blocksWide, the_blocksHigh,
				Collections.<FlightPath> emptyList(), the_poolSize);
	}
	
	public void addGenerator(final BlockGenerator the_generator, double length) {
		my_generators.add(the_generator);
		my_generatorLifeTimes.put(the_generator, length);
	}
	public void addGenerator(final BlockGenerator the_generator) {
		my_generators.add(the_generator);
	}
	public void step(int timeDelta, double yPositionChange) {
		my_yOffset += yPositionChange;
	}
	public void fixedStep(int timeDelta, double yPositionChange) {
		my_fixedYOffset += yPositionChange;
		if (my_disposing) {
			return;
		}
		
		
		
		// get new row
		while (my_fixedYOffset > (double) my_scaleY) {
			
			// countdown on generators
			for (BlockGenerator generator : my_generatorLifeTimes.keySet()) {
				my_generatorLifeTimes.put(generator,
						my_generatorLifeTimes.get(generator) - my_scaleY);
			}
			// check to see if any generators died
			if (!my_generatorLifeTimes.isEmpty()) {
				for (Iterator<BlockGenerator> it = my_generators.iterator(); it.hasNext();) {
					final BlockGenerator generator = it.next();
					if (my_generatorLifeTimes.containsKey(generator)
							&& my_generatorLifeTimes.get(generator) <= 0) {
						it.remove();
						my_generatorLifeTimes.remove(generator);
					}
				}
			}
			
			
			
			
			
			
			
			my_fixedYOffset -= (double) my_scaleY;
			my_yOffset -= (double) my_scaleY;
			my_bottom = (my_bottom + 1) % my_totalBlocksHigh;
			
			
			
			
			// generate safe path
			final int pathIndex = (my_bottom + my_pathRow) % my_totalBlocksHigh;
			for (int col = 0; col < my_blocksWide; col++) {
				if (my_blocks[pathIndex][col].isOccupied()) {
					my_pool.free(my_blocks[pathIndex][col]);
				}
				my_blocks[pathIndex][col] = Block.EMPTY_BLOCK;
			}
			
			for (FlightPath path : my_paths) {
				double min = my_width;
				double max = 0;
				final double stopAfter = my_extraHeight - ((my_pathRow) * my_scaleY - my_height - my_fixedYOffset)
						+ path.getWidth();
						
//						my_extraTop + my_yOffset + my_scaleY + path.getLength()
//						+ path.getWidth();
				
				double y = path.getFirstY();
				for (int i = 0; i < path.size(); i++) {
					path.get(i, my_flightPoint);
					if (y > stopAfter) {
						break;
					}
					y += my_flightPoint.getY();
					min = Math.min(min, my_flightPoint.getX());
					max = Math.max(max, my_flightPoint.getX());
				}
				min -= path.getWidth() / 2;
				max += path.getWidth() / 2;
				min /= my_scaleX;
				max /= my_scaleX;
				min = Math.max(min,  0);
				max = Math.min(max, my_blocksWide);
				for (int i = (int)Math.floor(min); i < (int) Math.ceil(max); i++) {
					if (my_blocks[pathIndex][i] != Block.PRIMARY_PATH) {
						my_blocks[pathIndex][i] = path.getBlock();
					}
				}
			}
			
			// construct section of layer to give to generators
			final Block[][] extra = new Block[my_genAreaRows][my_blocksWide];
			for (int i = 0; i < my_genAreaRows; i++) {
				extra[i] = my_blocks[(i + my_bottom + my_genAreaStart) % my_totalBlocksHigh];
			}
			
			// generate new blocks
			for (BlockGenerator generator : my_generators) {
				generator.generate(extra);
			}
			
			// Style the blocks
			// TODO this will be done somewhere else.
			final int fixedRow = (my_bottom + my_fixedAreaStart) % my_totalBlocksHigh;
			for (int col = 0; col < my_blocksWide; col++) {
				final Block current = my_blocks[fixedRow][col];
				if (Block.isOccupied(current)) {
					final Block block = my_pool.newObject();
					block.markImageAsDirty();
					block.setStyle(current.getStyle());
					block.setColor(current.getColor());
					block.setTinter(current.getTinter());
					
					final int oneUp = (fixedRow + 1) % my_totalBlocksHigh;
					final int oneDown = (fixedRow - 1 + my_totalBlocksHigh) % my_totalBlocksHigh;
					block.setNeighbors(
							// top
							my_blocks[oneUp][col].canJoin(current) ? NeighborState.FULL : NeighborState.EMPTY,
							// right
							col == my_blocksWide - 1 || my_blocks[fixedRow][col + 1].canJoin(current)
									? NeighborState.FULL : NeighborState.EMPTY,
							// bot
							my_blocks[oneDown][col].canJoin(current)
									? NeighborState.FULL : NeighborState.EMPTY,
							// left
							col == 0 || my_blocks[fixedRow][col - 1].canJoin(current)
									? NeighborState.FULL : NeighborState.EMPTY);
					current.getStyle().prepare(block);
					my_blocks[fixedRow][col] = block;
				}
			}

		}
	}
	/**
	 * Tests to see if the given point collides with a block.  If it does, the block is destroyed
	 * and BlockParts are returned.  Otherwise null is returned.
	 * @param x
	 * @param y
	 * @return
	 */
	public List<BlockPart> collideAndDestroy(double x, double y) {
		final List<BlockPart> parts = new ArrayList<BlockPart>();
		int shipX = getColumnFromCoordinate(x);
	    int shipY = getRowFromCoordinate(y);
	    BlockPart part = destroyBlock(shipY, shipX, Color.RED);
	    if (part != null) {
	    	parts.add(part);
	    }
		return parts;
	}
	// this code coudl probably be used in destroyblock and collideAndDestroy
	public boolean pointCollides(double the_x, double the_y) {
		int col = getColumnFromCoordinate(the_x);
	    int y = getRowFromCoordinate(the_y);
	    if (col < my_blocksWide && col >= 0 && y < my_totalBlocksHigh && y >= 0) {
	    	final int row = (my_bottom + y) % my_totalBlocksHigh;
			if (Block.isOccupied(my_blocks[row][col])) {
				return true;
			}
	    }
	    return false;
	}
	private int getRowFromCoordinate(double y) {
		return (int)((y + my_yOffset) / my_scaleY);
	}
	private int getColumnFromCoordinate(double x) {
		return (int)(x / my_scaleX);
	}
	private BlockPart destroyBlock(int y, int col, Color color) {
		final BlockPart part;
		if (col >= my_blocksWide || col < 0 || y >= my_playAreaRows || y < 0) {
			part = null;
		} else {
			final int row = (my_bottom + y) % my_totalBlocksHigh;
			if (Block.isOccupied(my_blocks[row][col])) {
		    	my_blocks[row][col] = Block.EMPTY_BLOCK;
		    	part = BlockPart.explodeBlock(0, 0, col * my_scaleX, my_height - (y + 1) * my_scaleY + (float)my_yOffset,
		    			my_scaleX, my_scaleY, color).get(0);
		    	
		    	// set neighbors as this being destroyed
		    	markBlockAsDamaged(y, col - 1, 1);
		    	markBlockAsDamaged(y, col + 1, 3);
		    	markBlockAsDamaged(y + 1, col, 2);
		    	markBlockAsDamaged(y - 1, col, 0);
		    	
		    	
			} else {
				part = null;
			}
		}
		return part;
	}
	/**
	 * Destroys any blocks inside the given rectangle
	 * @param the_left
	 * @param the_top
	 * @param the_width
	 * @param the_height
	 * @return representations of any destroyed blocks.
	 */
	public List<BlockPart> checkRectangle(final double the_left, final double the_top,
			final double the_width,
			final double the_height) {
		List<BlockPart> parts = new ArrayList<BlockPart>();
		int left = getColumnFromCoordinate(the_left);
		int right = getColumnFromCoordinate(the_left + the_width);
		int top = getRowFromCoordinate(my_height - the_top);
		int bot = getRowFromCoordinate(my_height - the_top - the_height);
		for (int row = bot; row <= top; row++) {
			for (int col = left; col <= right; col++) {
				BlockPart part = destroyBlock(row, col, Color.WHITE);
				if (part != null) {
					parts.add(part);
				}
			}
		}
		// update the blocks around any destroyed blocks to show them damaged.
		// That is, if a block next to the destroyed area is marked as 
		// having a neighbor in the area, it should be marked as having
		// a destroyed neighbor.
		
//		if (left > 0) {
//			for (int row = bot; row <= top; row++) {
//				markBlockAsDamaged(row, left - 1, 1);
//			}
//		}
//		if (right + 1< my_blocksWide) {
//			for (int row = bot; row <= top; row++) {
//				markBlockAsDamaged(row, right + 1, 3);
//			}
//		}
//		for (int col = left; col <= right; col++) {
//			markBlockAsDamaged(top + 1, col, 2);
//			markBlockAsDamaged(bot - 1, col, 2);
//		}
		
		return parts;
	}
	private void markBlockAsDamaged(final int the_row, final int the_col, final int the_direction) {
		if (the_col >= my_blocksWide || the_col < 0 || the_row >= my_playAreaRows || the_row < 0) { // TODO this checking can perhaps be better done in checkRectangle
			// the_row = my_playAreaRows is not allowed
			return;
		} else {
			final int row = (my_bottom + the_row) % my_totalBlocksHigh;
			final Block block = my_blocks[row][the_col];
			if (block.getNeighborState(the_direction) == NeighborState.FULL) {
				block.setNeighborState(the_direction, NeighborState.DESTROYED);
				block.getStyle().prepare(block);
			}
		}
	}
	
	public void render(GraphicsWrapper g) {
	    
	    
	    // draw squares
		final int drawHeight = my_totalBlocksHigh;//my_blocksHigh + 1;
	    for (int i = 0; i < drawHeight; i++) {
	    	for (int col = 0; col < my_blocksWide; col++) {
	    		final int row = (my_bottom + i) % my_totalBlocksHigh;
	    		if (my_blocks[row][col].isOccupied()) {// || my_blocks[row][col].isPath()) {
	    			
	    			
	    			my_blocks[row][col].render(g, (int)(col * my_scaleX), (int)((my_blocksHigh - i - 1) * my_scaleY + my_yOffset),
	    					(int) (my_scaleX + .5), (int) (my_scaleY + .5));

	    		}
	    	}
	    }
//	    g.setColor(Color.RED);
//	    g.fillRect(0, (int)(my_height + my_yOffset - my_genAreaStart * my_scaleY), 50, 50);
	    
	    
	    
//	    g2.setColor(Color.GREEN);
//	    g2.fillRect((int)0, (int)(my_extraTop+ my_yOffset), (int) my_scaleX, (int) my_scaleY * (my_totalBlocksHigh - my_blocksHigh - 1));
	    
	    
	    
	}
	/**
	 * @return the least negative horizontal boundary of where blocks are generated,
	 * in user space.
	 */
	public double getGenAreaStart() {
		return my_height + my_yOffset - my_genAreaStart * my_scaleY;
	}
	/**
	 * How far the area where paths are generated is from the bottom
	 * of the generation area.
	 * @return
	 */
	public double getPathOffset() {
		return (my_pathRow - my_genAreaStart) * my_scaleY;
	}
	public double getBlockWidth() {
		return my_scaleX;
	}
	public double getBlockHeight() {
		return my_scaleY;
	}
	

	@Override
	public Block createObject() {
		if (my_disposing) {
			return null;
		}
		return new Block();
	}
		
	public void dispose() {
		for (int row = 0; row < my_blocks.length; row++) {
			for (int col = 0; col < my_blocks[row].length; col++) {
				my_blocks[row][col].dispose();
			}
		}
		my_disposing = true;
		Block block = my_pool.newObject();
		while (block != null) {
			block.dispose();
			block = my_pool.newObject();
		}
	}
}
